package com.googlecode.lightest.core

import java.util.concurrent.atomic.AtomicInteger
import org.codehaus.groovy.runtime.StackTraceUtils
import org.testng.internal.ResultMap
import org.testng.IReporter
import org.testng.IResultMap
import org.testng.ISuite
import org.testng.ITestContext
import org.testng.ITestResult
import org.testng.Reporter
import org.testng.reporters.FailedReporter
import org.testng.TestListenerAdapter
import org.testng.xml.XmlSuite

/**
 * This class is responsible for wiring task dispatchers to testcases, and
 * recording the results of running tasks. It also notifies any registered
 * reporters to generate reports at appropriate points in the test execution
 * lifecycle.
 * 
 * It supports two styles of reporters: ones that implement IReporter, and ones
 * that implement ILightestReporter. The former is used to create a single
 * report at the end of the suite run, while the latter may be updated
 * throughout the run, as tests are completed.
 */
class LightestTestListener extends TestListenerAdapter
    implements ILightestTestListener
{
    public static final String ATTR_STRATEGY = 'strategy'
    
    TestRegistry registry
    String outputDir
    
    /** the default implementation is an in-memory Map. */
    Map<ITestResult, List<ITaskResult>> taskResultMap
    
    private InheritableThreadLocal<TestNGContext> testngContext
    private Map<Class, ITestEnvironment> envMap
    private Map<ITestResult, Integer> idMap
    private IDispatcherAssignmentStrategy strategy
    private List reporters
    private AtomicInteger resultCounter
    private XMLReporter xmlReporter
    private PendingReporter pendingReporter
    private FailedReporter failedReporter

    LightestTestListener() {
        testngContext = new InheritableThreadLocal<TestNGContext>()
        envMap = Collections.synchronizedMap([:])
        reporters = []
        registry = new TestRegistry()
        taskResultMap = Collections.synchronizedMap([:])
        idMap = Collections.synchronizedMap([:])
        resultCounter = new AtomicInteger(1)
        xmlReporter = new XMLReporter()
        pendingReporter = new PendingReporter()
        failedReporter = new FailedReporter()
    }

    /**
     * Records the mapping between a test class and the environment in which
     * the test was performed.
     * 
     * @param testClass
     * @param env
     */
    void addEnvironmentMapping(Class testClass, ITestEnvironment env) {
        if (envMap[testClass] == null) {
            envMap[testClass] = env
        }
    }
    
    void setDispatcherAssignmentStrategy(IDispatcherAssignmentStrategy strategy)
    {
        this.strategy = strategy
    }
    
    /**
     * Implemented to satisfy the ISuiteListener interface. A no-op.
     * 
     * @param suite
     */
    void onFinish(ISuite suite) {}
    
    /**
     * Generates the base report for all registered ILightestReporter's .
     * 
     * @param suite
     */
    void onStart(ISuite suite) {
        reporters.each {
            if (it instanceof ILightestReporter) {
                it.updateReport(suite, registry, outputDir)
            }
        }
    }

    /**
     * Ensures the list of failed tests maintained by the TestNG TestRunner
     * implementation underlying the ITestContext reflects the actual test
     * result status. We do this in the listener because the list is
     * manipulated by the TestNG Invoker, which we can't get our hands around.
     *
     * Yes, this is hackish - we're seriously meddling with the TestRunner
     * internals - but we've got to do it!
     *
     * @param context
     */
    @Override
    void onFinish(ITestContext context) {
        // TODO - add synchronization here?
        def testRunner = (org.testng.TestRunner) context
        IResultMap passedTests = new ResultMap()

        for (testResult in testRunner.getPassedTests().getAllResults()) {
            if (testResult.getStatus() == ITestResult.FAILURE) {
                testRunner.addFailedTest(testResult.getMethod(), testResult)
            }
            else {
                passedTests.addResult(testResult, testResult.getMethod())
            }
        }

        // TODO - figure out why I added this, and write a test for it ...
        if (passedTests.size() != testRunner.getPassedTests().size()) {
            // update the TestRunner internal variable! Yikes!
            testRunner.m_passedTests = passedTests
        }

        super.onFinish(context)
        testngContext.set(null)
    }

    @Override
    void onStart(ITestContext context) {
        super.onStart(context)
        context.setAttribute(ATTR_STRATEGY, strategy)
        testngContext.set(new TestNGContext(context))
    }

    @Override
    void onTestFailure(ITestResult result) {
        super.onTestFailure(result)
        
        if (result.getThrowable() != null) {
            StackTraceUtils.deepSanitize(result.getThrowable())
        }
        
        onTestFinish(result)
        taskResultMap.remove(result)  // memory
    }

    @Override
    void onTestSkipped(ITestResult result) {
        super.onTestSkipped(result)
        onTestFinish(result)
    }

    @Override
    void onTestStart(ITestResult result) {
        super.onTestStart(result)
        taskResultMap[result] = []
    }

    /**
     * Instead of simply delegating to the TestListenerAdapter, we first check
     * if the tasks corresponding to the test method were all OK. If not, mark
     * the result as failed, and report failure to the adapter.
     *
     * This logic is housed here for convenience, because we don't have many
     * opportunities to alter the result status. Assuming this listener
     * implementation precedes all others in the list of listeners maintained
     * by the TestNG instance, the desired status should be propagated
     * correctly.
     *
     * @param result
     */
    @Override
    void onTestSuccess(ITestResult result) {
        for (taskResult in taskResultMap[result]) {
            if (taskResult.getStatus() != ITaskResult.STATUS_OK) {
                result.setStatus(ITestResult.FAILURE)
                onTestFailure(result)
                return
            }
        }

        super.onTestSuccess(result)
        onTestFinish(result)
        taskResultMap.remove(result)  // memory
    }

    /**
     * Recordes test run information, updates testng-results.xml, and invokes
     * the report for all registered ILightestReporter's . The XML file is
     * guaranteed to be created before the other reporters are invoked, and
     * exist during their invocation (unless they delete it!) Override this to
     * add functionality that is common to test failures, successes, and skips.
     * Make sure to invoke the superclass' method afterwards.
     *
     * @param result
     */
    void onTestFinish(ITestResult result) {
        def id = resultCounter.getAndIncrement()
        
        registry.resolve(testngContext.get(), result, id)
        
        def lightestResult = new LightestTestResult(result, id)
        
        lightestResult.env = envMap[result.testClass.realClass]
        lightestResult.taskResults = taskResultMap[result]
        
        // this method may be called by multiple threads, so synchronize
        // reporter method invocations
        
        synchronized (xmlReporter) {
            xmlReporter.setRegistry(registry)
            generateReport(xmlReporter)
            generateReport(pendingReporter)
            generateReport(failedReporter)
            
            reporters.each {
                if (it instanceof ILightestReporter) {
                    it.updateReport(lightestResult, registry, outputDir)
                }
            }
        }
    }

    /**
     * Adds the ITaskResult to the list kept for each ITestResult. Only root-
     * level results are added to the list; child results are linked through
     * the root.
     *
     * @param taskResult
     */
    void onTaskComplete(ITaskResult taskResult) {
        if (taskResult.parent == null) {
            // access Reporter's private static method
            ITestResult result = Reporter.getCurrentTestResult()
            taskResultMap[result] << taskResult
        }
    }
    
    /**
     * A wrapper for the parameterized generateReport() method.
     * 
     * @param reporter
     */
    protected void generateReport(IReporter reporter) {
        List<ITestContext> contexts = getTestContexts()
        Set<ISuite> uniqueSuites = new LinkedHashSet<ISuite>()
        List<ISuite> suites = []
        List<XmlSuite> xmlSuites = []
        
        contexts.each {
            uniqueSuites << it.getSuite()
        }
        
        // the ordering of the Lists of ISuite and XmlSuite objects are
        // expected to correspond to each other
        
        uniqueSuites.each {
            suites << it
            xmlSuites << it.getXmlSuite()
        }
        
        generateReport(reporter, xmlSuites, suites)
    }
    
    /**
     * Invokes the specified reporter using all available information available
     * to this listener. This method is aware of the ITestRegistryAcceptor,
     * interface, and will set the current registry on reporters that implement
     * it.
     * 
     * @param reporter   the reporter to run
     * @param xmlSuites
     * @param suites
     */
    protected void generateReport(IReporter reporter, List<XmlSuite> xmlSuites,
        List<ISuite> suites) {
            
        if (reporter instanceof ITestRegistryAcceptor) {
            reporter.setRegistry(registry)
        }
        if (reporter instanceof IReporter) {
            reporter.generateReport(xmlSuites, suites, outputDir)
        }
    }
    
    /**
     * Invokes generateReport() on all registered IReporter reporters. The 
     * testng-results.xml and testng-failed,xml XML files are guaranteed to be
     * generated before the other reports are generated. This method will be
     * invoked by the TestNG engine at the end of the suite run.
     * 
     * @param xmlSuites
     * @param suites
     * @param outputDirectory  this is ignored; the current value of outputDir
     *                         on this object is used instead
     */
    void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites,
        String outputDirectory)
    {
        generateReport(failedReporter, xmlSuites, suites)
        
        reporters.each {
            if (it instanceof IReporter) {
                generateReport(it, xmlSuites, suites)
            }
        }
    }
    
    void registerReporter(Object reporter) {
        reporters << reporter
    }
    
    void initializeReporters(String configText) {
        reporters.each {
            if (it instanceof ILightestReporter) {
                it.setConfigText(configText)
                it.generateBaseReport(registry, outputDir)
            }
        }
    }
}

